跳到主要内容

Go 使用 TLS 协议-基本使用

TLS 介绍

传输层安全协议(Transport Layer Security,缩写:TLS),及其前身安全套接层(Secure Sockets Layer,缩写:SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。

SSL包含记录层(Record Layer)和传输层,记录层协议确定了传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密钥作为会谈密钥(Session key)。这个会谈密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。

TLS历史

1994年早期,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。 1994年11月,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。 1996年11月,SSL 3.0版问世,得到大规模应用。 1999年1月,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。 2006年4月和2008年8月,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版。 现在正在制定 tls 1.3。

生成证书

服务端证书生成

# 生成私钥:
openssl genrsa -out key.pem 2048
# 生成证书:
openssl req -new -x509 -key key.pem -out cert.pem -days 3650

客户端的证书生成

openssl req -new -x509 -key client.key -out client.pem -days 3650

除了"服务端证书",在某些场合中还会涉及到"客户端证书"。所谓的"客户端证书"就是用来证明客户端访问者的身份。 比如在某些金融公司的内网,你的电脑上必须部署"客户端证书",才能打开重要服务器的页面。

简单的 HTTPS 使用

HTTPS 要使用 http 连接通道技术。首先客户端发送请求使用 HTTP CONNECT 方法来创建一个连接客户端和目标服务器的通道。

当这个通道的两个 TCP 连接已经就绪,客户开始常规的和目标服务器 TLS 建立握手之后就开始发送请求和接受响应。

如下示例代码

import (
"log"
"net/http"
)

func main() {
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r)
}),
}

err := srv.ListenAndServeTLS("key/cert.pem", "key/key.pem")
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

别忘了 https

简单的使用 TLS 连接

服务端

package main

import (
"bufio"
"crypto/tls"
"log"
"net"
)

func main() {
log.SetFlags(log.Lshortfile)
cer, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
log.Println(err)
return
}

config := &tls.Config{Certificates: []tls.Certificate{cer}}
ln, err := tls.Listen("tcp", ":8000", config)
if err != nil {
log.Println(err)
return
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConnection(conn)
}
}

func handleConnection(conn net.Conn) {
defer conn.Close()
r := bufio.NewReader(conn)
for {
msg, err := r.ReadString('\n')
if err != nil {
log.Println(err)
return
}
println(msg)
n, err := conn.Write([]byte("world\n"))
if err != nil {
log.Println(n, err)
return
}
}
}

首先从上面我们创建的服务器私钥和pem文件中得到证书cert,并且生成一个tls.Config对象。这个对象有多个字段可以设置,本例中使用它的默认值。

然后用 tls.Listen 开始监听客户端的连接,accept 后得到一个 net.Conn,后续处理和普通的 TCP 程序一样。

客户端

package main

import (
"crypto/tls"
"log"
)

func main() {
log.SetFlags(log.Lshortfile)
conf := &tls.Config{
InsecureSkipVerify: true,
}

conn, err := tls.Dial("tcp", "127.0.0.1:8000", conf)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
n, err := conn.Write([]byte("hello\n"))
if err != nil {
log.Println(n, err)
return
}
buf := make([]byte, 100)
n, err = conn.Read(buf)
if err != nil {
log.Println(n, err)
return
}
println(string(buf[:n]))
}

InsecureSkipVerify 用来控制客户端是否证书和服务器主机名。如果设置为 true,则不会校验证书以及证书中的主机名和服务器主机名是否一致。

因为在我们的例子中使用自签名的证书,所以设置它为true,仅仅用于测试目的。

客户端证书的使用

在有的情况下,需要双向认证,服务器也需要验证客户端的真实性。在这种情况下,我们需要服务器和客户端进行一点额外的配置。

服务器端:

package main
import (
"bufio"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net"
)
func main() {
cert, err := tls.LoadX509KeyPair("server.pem", "server.key")
if err != nil {
log.Println(err)
return
}
certBytes, err := ioutil.ReadFile("client.pem")
if err != nil {
panic("Unable to read cert.pem")
}
clientCertPool := x509.NewCertPool()
ok := clientCertPool.AppendCertsFromPEM(certBytes)
if !ok {
panic("failed to parse root certificate")
}
config := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientCertPool,
}
ln, err := tls.Listen("tcp", ":443", config)
if err != nil {
log.Println(err)
return
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
r := bufio.NewReader(conn)
for {
msg, err := r.ReadString('\n')
if err != nil {
log.Println(err)
return
}
println(msg)
n, err := conn.Write([]byte("world\n"))
if err != nil {
log.Println(n, err)
return
}
}
}

因为需要验证客户端,我们需要额外配置下面两个字段:

ClientAuth:   tls.RequireAndVerifyClientCert,
ClientCAs: clientCertPool,

然后客户端也配置这个 clientCertPool:

package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
)
func main() {
cert, err := tls.LoadX509KeyPair("client.pem", "client.key")
if err != nil {
log.Println(err)
return
}
certBytes, err := ioutil.ReadFile("client.pem")
if err != nil {
panic("Unable to read cert.pem")
}
clientCertPool := x509.NewCertPool()
ok := clientCertPool.AppendCertsFromPEM(certBytes)
if !ok {
panic("failed to parse root certificate")
}
conf := &tls.Config{
RootCAs: clientCertPool,
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", "127.0.0.1:443", conf)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
n, err := conn.Write([]byte("hello\n"))
if err != nil {
log.Println(n, err)
return
}
buf := make([]byte, 100)
n, err = conn.Read(buf)
if err != nil {
log.Println(n, err)
return
}
println(string(buf[:n]))
}

References

go tls实现TLS 服务器和客户端通讯 HTTP(S) Proxy in Golang in less than 100 lines of code